跳到主要内容

在 JavaScript 中,继承是实现代码复用和类型扩展的核心机制。组合继承(Combination Inheritance) 和寄生继承(Parasitic Inheritance)是两种经典模式

组合继承(Combination inheritance)

组合继承是结合了构造函数继承和原型链继承的方法,是早期最常用的继承方式。

构造函数继承解决了实例属性的问题,原型链继承解决了方法共享的问题。

但是组合继承有一个缺点,就是会调用两次构造函数:一次是在创建子类继承时;一次是在子类构造函数中,这将导致子类原型上有一份多余的父类实例属性。

function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function () {
console.log(this.name);
};

function Child(name, age) {
// 构造函数继承:复制父类实例属性到子类实例
Parent.call(this, name);
this.age = age;
}

// 原型链继承:子类原型指向父类实例(共享方法)
Child.prototype = new Parent();

// 修复 constructor 指向
Child.prototype.constructor = Child;

Child.prototype.sayAge = function () {
console.log(this.age);
};

优点

  • 实例属性独立(通过构造函数复制),避免了引用类型共享的问题
  • 方法共享(通过原型链),节省空间

缺点

  • 父类构造函数被调用两次:第一次是在 new Parent() (创建子类原型时),第二次是在 Parent.call(this) (子类构造函数中)

这样会导致子类原型上存在冗余的父类实例属性,浪费内存且可能引发意外覆盖。

寄生继承(Parasitic inheritance)

寄生继承基于原型链继承,但通过一个新对象来增强原型,避免了组合继承两次调用父类构造函数的问题。寄生继承通常通过使用 Object.create() 来创建父类原型的副本,然后添加额外的属性和方法,最后返回这个新对象作为子类的原型。

function createAnother(original) {
// 创建原对象的副本(原型链继承)
const clone = Object.create(original);
// 增强副本(添加新方法)
clone.sayHi = function () {
console.log('Hi');
};
// 返回增强后的对象
return clone;
}

const parent = {
name: 'Parent',
colors: ['red', 'blue'],
};

// child 是增强后的 parent 副本
const child = createAnother(parent);
// "Hi"
child.sayHi();
// 不影响原 parent 对象(引用类型独立?不,原型链共享!)
child.colors.push('green');

优点

  • 避免了直接调用父类构造函数,减少冗余属性
  • 灵活增加对象(添加/修改方法)

缺点

  • 本质上是原型链继承,引用类型属性仍共享
  • 更是个“对象增强”场景,而非类式继承

寄生组合式继承

组合继承的优化版本,通过寄生方式继承原型,避免父类构造函数被调用多次。

优先使用 Object.create() 创建纯净的[原型链]:

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 手动修改 constructor 的指向

实例

/// 组合 + 寄生继承
function inheritPrototype(Child, Parent) {
// 创建父类原型的副本(避免实例化父类)
const prototype = Object.create(Parent.prototype);
// 修复 constructor 指向
prototype.constructor = Child;
// 子类原型指向副本
Child.prototype = prototype;
}

function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function () {
console.log(this.name);
};

function Child(name, age) {
// 仅调用一次父类构造函数(复制实例属性)
Parent.call(this, name);
this.age = age;
}

// 寄生继承原型(避免两次调用 Parent)
inheritPrototype(Child, Parent);

Child.prototype.sayAge = function () {
console.log(this.age);
};

优点

  • 父类构造函数仅调用一次:子类实例属性独立,原型链通过副本继承方法
  • 无冗余的属性,内存更高效
  • 兼容原型继承链继承和方法共享的特性

其他继承

除了组合继承和寄生继承外,还有原型链继承、构造函数继承、拷贝继承、ES6 class 继承等多种模式。

原型链继承(prototype chain inheritance)

  • 核心逻辑:让子类的原型对象(prototype)指向父类的实例,通过原型链访问父类的属性和方法
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};

function Child() {}
// 子类原型指向父类实例
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;

const child1 = new Child();
child1.sayName(); // "Parent"
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]

const child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green"](引用类型共享)

优点

  • 直接简单,通过原型链自动继承原型上的方法
  • 子类实例可直接访问父类原型的属性

缺点

  • 引用类型属性共享:父类实例的引用类型属性会被所有的实例共享,修改一个实例的属性将影响其他的实例
  • 无法向父类构造函数传参:父类实例化时无法接收子类传递的参数(因为 new Parent() 是固定的)
  • 父类构造函数副作用:如父类构造函数内有副作用(如修改全局状态),会被意外触发

构造函数继承(Constructor inheritance)

构造函数继承是经典继承。

  • 核心逻辑:在子类构造函数中调用父类构造函数(使用 call/apply),将父类属性绑定到子类实例上。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};

function Child(name, age) {
// 关键:调用父类构造函数,绑定属性到子类实例
Parent.call(this, name);
this.age = age;
}

const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]

const child2 = new Child('Bob', 12);
console.log(child2.colors); // ["red", "blue"](引用类型独立)
child2.sayName(); // 报错:sayName 未定义(未继承原型方法)

优点

  • 实例属性独立:父类实例属性通过 Parent.call(this) 绑定到子类实例,避免了引用类型共享问题
  • 支持向父类传参:可通过 Parent.call(this, arg) 向父类构造函数传递参数
  • 无父类构造函数副作用:父类构造函数仅在子类实例化时调用,无额外的副作用

缺点

  • 无法继承父类原型方法:父类上的原型方法无法被子类实例访问
  • 方法无法复用:若父类包含多个实例方法,需在子类构造函数中重复定义,无法复用

拷贝继承(Copy inheritance)

  • 核心逻辑:通过遍历父类实例和原型的属性,手动复制到子类实例或原型上
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};

function Child() {
const parent = new Parent();
// 复制父类实例属性到子类实例(浅拷贝)
Object.assign(this, parent);
}
// 复制父类原型方法到子类原型(浅拷贝)
Child.prototype = Object.assign({}, Parent.prototype);

Child.prototype.constructor = Child;

const child = new Child();
child.sayName(); // "Parent"
child.colors.push('green');
console.log(child.colors); // ["red", "blue", "green"](引用类型仍共享)

优点

  • 属性完全独立:如使用深层拷贝,可完全避免引用类型共享问题
  • 灵活性高:可选择性复制需要的属性和方法

缺点

  • 性能差:深拷贝大对象时开销极大
  • 动态方法丢失:父类原型上动态新增的方法无法被复制
  • 实现复杂:需手动处理原型链和实例属性的拷贝,易出错

ES6 class 继承

  • 核心逻辑:ES6 引入 class 关键字,通过 extends 实现继承,底层基于 寄生组合式继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() {
console.log(this.name);
}
}

class Child extends Parent {
constructor(name, age) {
// 调用父类构造函数(必须先调用)
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}

const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]

const child2 = new Child('Bob', 12);
console.log(child2.colors); // ["red", "blue"](引用类型独立)
child2.sayName(); // "Bob"(继承父类原型方法)

优点

  • 语法简介:无需手动处理原型链、constructor 指向或 super 调用
  • 自动优化继承:底层使用继承组合式继承,无冗余属性,性能高效
  • 支持静态方法继承:父类静态方法(如 static fn())会被子类继承
  • 更符合直觉:类式语法更贴近于传统的面相对象语言

缺点

  • 依赖引擎支持:需要在支持 ES6 的环境中使用(现代浏览器或 Node.js ≥ 6)
  • 本质仍是原型继承:所有的方法仍定义在原型上,与 ES5 的继承无本质上的区别

需关注的点

    1. 谨慎使用[原型链]继承!直接继承另一个对象的原型将导致:
    • 原型链上的引用属性将会被所有的实例共享,可能导致一个实例修改了这个属性,影响到其他的实例;
    • 创建子类实例时,无法向父类构造函数传递参数
    1. 组合继承(伪经典继承):几个了构造函数和原型链继承,通常是被认为比较理想的继承方法。但,组合继承会调用两次父类构件函数(一次在创建子类原型时,一次在子类构造函数内部),导致子类的原型上多一分多余的父类实例属性。
    1. 原型继承:基于已有的对象创建新对象,而不必创建自定义类型。 Object.create() 就是基于原型式继承。适用于不需要单独创建构造函数的场景;
    1. 寄生式继承:创建一个仅用于封装继承过程的函数,在函数内部增强对象,最后返回对象。适合主要关注对象,而不在乎类型和结构函数的场景;
    1. 寄生组合继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不过为了制定子类型的原型而调用父类的构造函数,而是使用父类原型的一个副本。这是最理想的继承方式之一。
    1. 虽然 ES6 引入了 class 关键字,但它只是原型链继承的语法糖。class 的 extends 关键字实际上创建了一个原型链继承关系;
    1. 在 ES 中,子类构造函数必须先调用 super() 才能使用 thissuper 指向父类的原型对象(在方法中)或父类构造函数(在构造函数中);

继承对比

继承方式核心机制优点缺点适用场景
原型链继承子类原型指向父类实例简单,自动继承原型方法应用类型共享,无法传参简单原型方法继承
构造函数继承子类构造函数调用父类构造函数实例属性独立,支持传参无法继承原型方法需独立实例属性的场景
寄生继承创建父类副本并增强灵活增强对象引用类型共享,适合对象增强对象增强(非类继承)
组合继承构造函数 + 原型链实例独立 + 方法共享父类构造函数调用两次,原型冗余理解底层机制的学习场景
寄生组合式继承寄生 + 组合无冗余、高效实现稍复杂手动实现类继承的最优解
ES 6 class继承语法糖(底层寄生组合式)简介高效,支持静态方法依赖于 ES6 环境现代项目首选
拷贝继承手动复制属性/方法属性独立(深拷贝时)性能差,动态方法丢失(非继承式继承)特殊需求(如需隔离环境)